<template>
  <div id="global-uploader" :class="{ 'global-uploader-single': !global }">
    <uploader
      ref="uploaderRef"
      :auto-start="false"
      :file-status-text="fileStatusText"
      :options="initOptions"
      class="uploader-app"
      @complete="onUploadComplete"
      @filesAdded="onFilesAdded"
      @file-added="onFileAdded"
      @file-success="onFileSuccess"
      @file-error="onFileError"
    >
      <uploader-unsupport />
      <uploader-btn id="global-uploader-btn" ref="uploadBtnRef">选择文件</uploader-btn>
      <transition
        enter-active-class="animate__animated animate__slideInUp"
        leave-active-class="animate__animated animate__slideOutDown"
        name="slide-fade"
      >
        <uploader-list v-show="panelShow" class="no-select">
          <template #default="{ fileList }">
            <div :class="{ collapse }" class="file-panel">
              <div class="file-title">
                <div class="title">文件列表</div>
                <div class="operate">
                  <el-button :title="collapse ? '展开' : '折叠'" link @click="collapse = !collapse">
                    <SvgIcon
                      :name="collapse ? 'fullscreen-fill' : 'subtract-fill'"
                      height="18px"
                      width="18px"
                    />
                  </el-button>
                  <el-button v-show="!collapse" link title="关闭" @click="close">
                    <SvgIcon :name="'close-fill2'" height="20px" width="20px" />
                  </el-button>
                </div>
              </div>

              <ul class="file-list">
                <li v-for="file in fileList" :key="file.id" class="file-item">
                  <uploader-file
                    ref="files"
                    :class="['file_' + file.id, customStatus]"
                    :file="file"
                    :list="true"
                  />
                </li>
                <div v-if="!fileList.length" class="no-file"> 暂无待上传文件</div>
              </ul>
            </div>
          </template>
        </uploader-list>
      </transition>
    </uploader>
  </div>
</template>

<script>
  /**
   *  全局上传插件，两种调用方式
   *   1. 作为全局页面的组件，使用event bus
   *   调用方法：Bus.emit('openUploader', {params: {}, options: {}})
   *               params: 发送给服务器的额外参数；
   *               options：上传选项，目前支持 target、testChunks、mergeFn、accept
   *
   *   监听函数：Bus.on('fileAdded', fn); 文件选择后的回调
   *           Bus.on('uploadFileSuccess', fn); 文件上传成功的回调，监听后记得释放
   *
   *   2. 作为普通组件在单个页面中调用，使用props
   */
  import { computed, nextTick, onMounted, ref, watch } from 'vue'
  import { generateMD5 } from '../../utils/md5.js'
  import Bus from '../../utils/bus.js'
  import { ElMessage } from 'element-plus'
  import SvgIcon from '@/components/SvgIcon/SvgIcon.vue'
  import { skipUploadService, uploadSignService } from '@/api/file.js'

  export default {
    name: 'GlobalUploader',
    components: { SvgIcon },
    props: {
      global: {
        type: Boolean,
        default: true
      },
      params: {
        type: Object
      },
      options: {
        type: Object
      }
    },

    emits: ['fileAdded', 'uploadFileSuccess'],

    setup(props, { emit }) {
      const customStatus = ref('')
      const panelShow = ref(false)
      const collapse = ref(true)
      const uploaderRef = ref()
      const uploadBtnRef = ref()
      const userId = ref(0)
      const parentId = ref(0)
      const uploader = computed(() => uploaderRef.value.uploader)
      let customParams = {}

      const initOptions = {
        target: '',
        testChunks: false, // 是否分片 - 不分片
        simultaneousUploads: 40, // 支持并发上传数量
        allowDuplicateUploads: false, // 上传过得文件不可以再上传
        chunkSize: '102400000000' // 块大小
      }
      // chunkSize: 1024 * 1024 * 1024 // 块大小

      const fileStatusText = {
        success: '上传成功',
        error: '上传失败',
        uploading: '上传中',
        paused: '已暂停',
        waiting: '等待上传'
      }

      watch(
        () => props.params,
        data => {
          if (data) {
            customParams = data
          }
        }
      )

      watch(
        () => props.options,
        data => {
          if (data) {
            setTimeout(() => {
              customizeOptions(data)
            }, 0)
          }
        }
      )

      // 所有文件上传成功
      function onUploadComplete() {
        Bus.emit('allFileUploadSuccess')
      }

      // 检查文件数量的函数
      function onFilesAdded(files, fileList) {
        if (fileList.length > 15) {
          panelShow.value = false
          collapse.value = false
          ElMessage.warning('上传取消，最多支持同时上传15个文件！')
          return false
        }
      }

      // 自定义options
      function customizeOptions(opts) {
        if (opts.target) {
          uploader.value.opts.target = opts.target
        }

        if (opts.userId) {
          userId.value = opts.userId
        }

        if (opts.parentId) {
          parentId.value = opts.parentId
        }
      }

      // 已添加的文件
      async function onFileAdded(file) {
        panelShow.value = true
        collapse.value = false
        trigger('fileAdded')
        // 计算MD5
        const md5 = await computeMD5(file)
        // 秒传逻辑
        const res = await skipUploadService(file.file.name, md5, parentId.value, file.file.size)
        if (res.data === '文件秒传') {
          statusSet(file.id, 'skipUpload')
          file.pause()
          nextTick(() => {
            const resumeButton = document.querySelector(`.file_${file.id} .uploader-file-resume`)
            const removeButton = document.querySelector(`.file_${file.id} .uploader-file-remove`)
            if (resumeButton) {
              resumeButton.style.display = 'none'
            }
            if (removeButton) {
              removeButton.style.display = 'block'
            }
          })
          Bus.emit('uploadFileSuccess')
          return
        } else if (res.code === 40302) {
          statusSet(file.id, 'expand')
          return
        }

        // 获取服务器签名
        const code = await getUploadSign(file, md5, parentId.value, userId.value)
        if (code !== 0) {
          statusSet(file.id, 'failed')
          return
        }

        // 设置文件参数
        file.params = {
          // 将额外的参数赋值到每个文件上，以不同文件使用不同params的需求
          ...customParams.value,
          // 设置实际的文件路径，替换 key 中的 ${filename}
          key: uploader.value.opts.query.key.replace('${filename}', file.name)
        }

        // 上传文件
        file.resume()
      }

      // 获取 OSS 上传策略和签名
      async function getUploadSign(file, identifier, parentId, userId) {
        const res = await uploadSignService(file.size)

        if (res.code === 40302) {
          statusSet(file.id, 'expand')
          return
        }

        const { accessId, policy, signature, dir, host, callback } = res.data
        // 设置上传参数
        uploader.value.opts.target = host
        uploader.value.opts.query = {
          OSSAccessKeyId: accessId,
          policy: policy,
          signature: signature,
          key: `${dir}/${file.name}`,
          callback: callback,
          success_action_status: '200',
          'x:uid': userId,
          'x:identifier': identifier,
          'x:pid': parentId
        }

        return res.code
      }

      function computeMD5(file) {
        // 文件状态设为"计算MD5"
        statusSet(file.id, 'md5')
        // 暂停文件
        file.pause()
        // 计算MD5时隐藏”开始“按钮
        nextTick(() => {
          const resumeButton = document.querySelector(`.file_${file.id} .uploader-file-resume`)
          if (resumeButton) {
            resumeButton.style.display = 'none'
          }
        })
        // 开始计算MD5
        return new Promise((resolve, reject) => {
          generateMD5(file, {
            onProgress(currentChunk, chunks) {
              // 实时展示MD5的计算进度
              nextTick(() => {
                document.querySelector(`.custom-status-${file.id}`).innerText =
                  '校验MD5 ' + ((currentChunk / chunks) * 100).toFixed(0) + '%'
              })
            },
            onSuccess(md5) {
              statusRemove(file.id)
              resolve(md5)
            },
            onError() {
              error(`文件${file.name}读取出错，请检查该文件`)
              file.cancel()
              statusRemove(file.id)
              reject()
            }
          })
        })
      }

      // 上传完成之后
      function onFileSuccess(rootFile, file, response) {
        try {
          const res = JSON.parse(response)
          if (res.code === 40302) {
            statusSet(file.id, 'expand')
            return false
          }

          Bus.emit('uploadFileSuccess')
          return true
        } catch (err) {
          console.error(err)
          statusSet(file.id, 'failed')
          return false
        }
      }

      function onFileError(rootFile, file, response) {
        error(response)
      }

      function close() {
        uploader.value.cancel()
        panelShow.value = false
        // collapse.value = true
      }

      /**
       * 新增的自定义的状态: 'md5'、'merging'、'transcoding'、'failed'
       * @param id
       * @param status
       */
      function statusSet(id, status) {
        const statusMap = {
          md5: {
            text: '校验MD5',
            bgc: '#fff'
          },
          skipUpload: {
            text: '文件秒传',
            bgc: '#E2EEFF'
          },
          failed: {
            text: '上传失败',
            bgc: '#FFE0E0'
          },
          expand: {
            text: '空间容量不足',
            bgc: '#FFE0E0'
          }
        }
        customStatus.value = status
        nextTick(() => {
          const statusTag = document.createElement('p')
          statusTag.className = `custom-status-${id} custom-status`
          statusTag.innerText = statusMap[status].text
          statusTag.style.backgroundColor = statusMap[status].bgc

          const statusWrap = document.querySelector(`.file_${id} .uploader-file-status`)
          statusWrap.appendChild(statusTag)

          if (status === 'skipUpload') {
            const skipUpload = document.querySelector(
              `.file-list .uploader-file.file_${id}.skipUpload .uploader-file-info`
            )
            skipUpload.style.background = statusMap[status].bgc
          }

          if (status === 'failed') {
            const failed = document.querySelector(
              `.file-list .uploader-file.file_${id}.failed .uploader-file-info`
            )
            failed.style.background = statusMap[status].bgc
          }

          if (status === 'expand') {
            const expand = document.querySelector(
              `.file-list .uploader-file.file_${id}.expand .uploader-file-info`
            )
            expand.style.background = statusMap[status].bgc
          }
        })
      }

      function statusRemove(id) {
        customStatus.value = ''
        nextTick(() => {
          const statusTag = document.querySelector(`.custom-status-${id}`)
          statusTag.remove()
        })
      }

      function trigger(e) {
        Bus.emit(e)
        emit(e)
      }

      function error(msg) {
        ElMessage.error(msg)
      }

      onMounted(() => {
        Bus.on('openUploader', async ({ params = {}, options = {} }) => {
          customParams = params
          customizeOptions(options)

          if (uploadBtnRef.value) {
            uploadBtnRef.value.$el.click()
          }
        })
      })

      return {
        initOptions,
        fileStatusText,
        customStatus,
        panelShow,
        collapse,
        uploaderRef,
        uploadBtnRef,
        onFileAdded,
        onFileSuccess,
        onFileError,
        close,
        onUploadComplete,
        onFilesAdded
      }
    }
  }
</script>

<style lang="less">
  @import '@/assets/variable.less';

  .uploader-file[status='success'] .uploader-file-remove {
    display: block;
  }

  #global-uploader {
    /* 添加动画相关样式 */

    .animate__animated {
      animation-duration: 0.4s;
      animation-fill-mode: both;
    }

    @keyframes slideInUp {
      from {
        transform: translate3d(0, 100%, 0);
        visibility: visible;
      }
      to {
        transform: translate3d(0, 0, 0);
      }
    }

    @keyframes slideOutDown {
      from {
        transform: translate3d(0, 0, 0);
      }
      to {
        visibility: hidden;
        transform: translate3d(0, 100%, 0);
      }
    }

    .animate__slideInUp {
      animation-name: slideInUp;
    }

    .animate__slideOutDown {
      animation-name: slideOutDown;
    }

    &:not(.global-uploader-single) {
      position: fixed;
      z-index: 20;
      right: 0;
      bottom: 0;
      box-sizing: border-box;
    }

    .uploader-app {
      width: 520px;
    }

    .file-panel {
      background-color: #fff;
      border: 1px solid #e2e2e2;
      border-radius: 7px 7px 0 0;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);

      .file-title {
        display: flex;
        height: 40px;
        line-height: 40px;
        padding: 0 15px;
        border-bottom: 1px solid #ddd;

        .operate {
          flex: 1;
          text-align: right;

          .el-button {
            --el-button-hover-link-text-color: @blue-color;

            + .el-button {
              margin-left: 8px;
            }
          }
        }
      }

      .file-list {
        position: relative;
        height: 240px;
        overflow-x: hidden;
        overflow-y: auto;
        background-color: #fff;
        transition: all 0.3s;

        .file-item {
          background-color: #fff;
        }
      }

      &.collapse {
        .file-title {
          background-color: #e7ecf2;
        }

        .file-list {
          height: 0;
        }
      }
    }

    .no-file {
      position: absolute;
      top: 45%;
      left: 50%;
      transform: translate(-50%, -50%);
      color: #999;

      svg {
        vertical-align: text-bottom;
      }
    }

    .uploader-file {
      &.md5 {
        .uploader-file-resume {
          display: none;
        }
      }
    }

    .uploader-file .uploader-file-actions > span {
      margin-right: 6px;
    }

    .custom-status {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      z-index: 1;
    }
  }

  /* 隐藏上传按钮 */
  #global-uploader-btn {
    position: absolute;
    clip: rect(0, 0, 0, 0);
  }

  .global-uploader-single {
    #global-uploader-btn {
      position: relative;
    }
  }
</style>
